//
//  APIsCPPParser.swift
//  Hermes
//
//  Created by FH on 2021/1/29.
//  Copyright © 2021 Agora. All rights reserved.
//

import Foundation

fileprivate let ParseParamValueEnumCase = try! Regex(pattern: #"([A-Za-z][\w:]+)($|,|\s+|\)|\|)"#, groupNames: ["name"])

enum PrivateParamJSONTipsType {
    case Boolean
    case Number
    case String
    case ObjectItem(_ key: String)
    case Raw
    case Object
    case List
}

extension PrivateParamJSONTipsType : Equatable {
    static func ==(lhs: PrivateParamJSONTipsType, rhs: PrivateParamJSONTipsType) -> Bool {
        let result = (lhs, rhs)
        switch result {
        case (.Boolean, .Boolean):
            return true
        case (.Number, .Number):
            return true
        case (.String, .String):
            return true
        case (.Raw, .Raw):
            return true
        case (.Object, .Object):
            return true
        case (.List, .List):
            return true
        default:
            return false
        }
    }
}

extension AstNode {
    var isCppPrivateParamRootClass: Bool {
        return self.value?.valueType ?? "" == "RootParamsClass"
    }
    
    var isCppPrivateParamSubClass: Bool {
        return self.value?.valueType ?? "" == "SubParamsClass"
    }
    
    var isCppPrivateParamField: Bool {
        if let valueNode = self.value, valueNode.valueType == "ParamField" && !valueNode.rawType.isEmpty {
            return true
        }
        return false
    }
    
    var cppPrivateParamNamespace: String {
        return self.value?.valueValidate ?? ""
    }
    
    var cppPrivateParamType: String {
        return self.value?.rawType ?? ""
    }
    
    var cppPrivateParamKey: String {
        return self.value?.typeEncoding ?? ""
    }
    
    var cppPrivateParamValue: String {
        return self.value?.value ?? ""
    }
    
    var cppPrivateParamJsonTypeTips: String {
        if let type = self.value?.rawType {
            return handlePrivateParamJsonType(type)
        }
        return ""
    }
    
    func getCppPrivateParamDefaultValue(with defaultValue: String?) -> String {
        let value = self.cppPrivateParamValue
        let jsonType = self.cppPrivateParamJsonTypeTips
        var result = defaultValue ?? value
        if jsonType.contains("Object") {
            if result.count == 0 {
                result = "{}"
            } else {
                result = value.replacingOccurrences(of: "any_document_t()", with: "{}")
            }
        } else if self.isListType(jsonType) && self.isObjectType(result) {
            result = result.replacingOccurrences(of: "{", with: "[").replacingOccurrences(of: "}", with: "]")
        } else if jsonType.contains("String") {
            if result == "std::string()" || result == "\"\"" {
                result = ""
            } else if result.contains("std::string(") || result.contains("\"\"") {
                result = value.replacingOccurrences(of: "std::string()", with: "\"\"")
                if result.contains(#"std::string(""#) {
                    result = result.replacingOccurrences(of: "std::string(", with: "").replacingOccurrences(of: ")", with: "")
                }
            }
        }
        // format list type
        if self.isListType(jsonType) {
            if !(self.isListType(result) || self.isObjectType(result)) {
                result = "[\(result)]"
            }
        } else if result.contains(",") && jsonType != "String" {
            // handle like `rtc.mute_all_policy`
            let split = result.components(separatedBy: ",")
            result = split[0]
        }
        return result
    }
    
    func getCppPrivateParamEnumValueMappingType(_ defaultValue: inout String, _ allEnumTypes: [String : AstNode]) -> AstNode? {
        if defaultValue.count > 0 {
            let jsonType = self.cppPrivateParamJsonTypeTips
            if jsonType.contains("Number") {
                var findEnumCases = [String : String]()
                var enumTypes = Set<AstNode>()
                for match in ParseParamValueEnumCase.findAll(in: defaultValue) {
                    if let name = match.group(named: "name") {
                        if name == "true" || name == "false" {
                            continue
                        }
                        findEnumCases[name] = ""
                        for (key, node) in allEnumTypes {
                            let isJsonType = node.handlePrivateParamJsonType(name).count > 0
                            if !isJsonType && key.contains(name) && node.type == .enumerate {
                                enumTypes.insert(node)
                                findEnumCases[name] = node.name
                                break
                            }
                        }
                    }
                }
                // handle unresolve identity
                for (enumCase, enumName) in findEnumCases where enumName.count == 0 {
                    if enumCase != "int" {
                        defaultValue = defaultValue.replacingOccurrences(of: enumCase, with: "")
                    }
                }
                // TODO: use `.value.blockSignature` to check enumType
                return enumTypes.count > 0 ? enumTypes.first! : nil
            }
        }
        return nil
    }
    
    // handle json tips types
    
    func getCppPrivateParamJsonTypes() -> [PrivateParamJSONTipsType] {
        var result = [PrivateParamJSONTipsType]()
        let jsonTipsType = self.cppPrivateParamJsonTypeTips
        if jsonTipsType == "Object" {
            result.append(.Object)
            self._getJsonComplexTypes(jsonTipsType, &result)
        } else if self.isListType(jsonTipsType) {
            result.append(.List)
            self._getJsonComplexTypes(jsonTipsType, &result)
        } else {
            result.append(.Raw)
            result.append(self._getJsonRawType(jsonTipsType))
        }
        return result
    }
    
    private func _getJsonRawType(_ jsonType: String) -> PrivateParamJSONTipsType {
        if jsonType == "Bool" {
            return .Boolean
        } else if jsonType == "Number" {
            return .Number
        } else if jsonType == "String" {
            return .String
        } else if jsonType == "Object" {
            return .Object
        }
        return .Raw
    }
    
    private func _getJsonComplexTypes(_ jsonType: String, _ result: inout [PrivateParamJSONTipsType]) {
        let type = jsonType.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "")
                    .replacingOccurrences(of: "[", with: "").replacingOccurrences(of: "]", with: "")
        let split = type.components(separatedBy: ",")
        if split.count > 0 {
            for item in split {
                result.append(self._getJsonRawType(item.trimmingCharacters(in: .whitespaces)))
            }
        }
    }
    
    private func handlePrivateParamJsonType(_ type: String) -> String {
        switch type {
        case "int", "float", "double", "uint32_t", "uint16_t", "unsigned int":
            return "Number"
        case "std::string", "const char *", "const void *":
            return "String"
        case "bool":
            return "Bool"
        case "any_document_t":
            return "Object"
        default:
            return handleComplexPrivateParamJsonType(type)
        }
    }
    
    private func handleComplexPrivateParamJsonType(_ type: String) -> String {
        if type.contains("<") {
            let split = type.components(separatedBy: "<")
            if split.count == 2 {
                let innerType = split[1].trimmingCharacters(in: [">"])
                return "[\(handlePrivateParamJsonType(innerType))]"
            }
        } else {
            let split = type.components(separatedBy: ",")
            if split.count > 1 {
                var complexType = "["
                for item in split {
                    complexType += "\(handlePrivateParamJsonType(item.trimmingCharacters(in: .whitespaces))), "
                }
                complexType = complexType.trimmingCharacters(in: [",", " "])
                complexType += "]"
                return complexType
            }
        }
        return ""
    }
    
    private func isListType(_ str: String) -> Bool {
        return str.hasPrefix("[") && str.hasSuffix("]")
    }
    
    private func isObjectType(_ str: String) -> Bool {
        return str.hasPrefix("{") && str.hasSuffix("}")
    }
}

extension Dictionary where Key == String, Value == AstNode {
    func fuzzySearchCppPrivateParams(_ needle: String?) -> [AstNode] {
        let allNodes = self.getCppPrivateParams()
        if let needle = needle {
            var result = [AstNode]()
            for node in allNodes {
                if node.cppPrivateParamKey.fuzzyMatch(needle) {
                    result.append(node)
                }
            }
            return result
        }
        return allNodes
    }
    
    func getCppPrivateParams() -> [AstNode] {
        var params = [AstNode]()
        for (_, node) in self where node.isCppPrivateParamRootClass {
            for subClass in node.children where subClass.isCppPrivateParamSubClass {
                for item in subClass.children where item.isCppPrivateParamField {
                    params.append(item)
                }
            }
        }
        // add params by manual
        let manualParams = AstNode(type: .class, name: "manual params")
        let paramLogFileNode = AstNode(type: .property, name: "")
        let paramLogFileValueNode = AstNodeValue()
        paramLogFileValueNode.valueType = "ParamField"
        paramLogFileValueNode.typeEncoding = "rtc.log_file"
        paramLogFileValueNode.rawType = "std::string"
        paramLogFileValueNode.valueValidate = "rtc::agora"
        paramLogFileValueNode.value = ""
        paramLogFileValueNode.blockSignature = nil
        paramLogFileNode.value = paramLogFileValueNode
        manualParams.children.append(paramLogFileNode)
        
        // sort params
        params.sort { (item1, item2) in
            return item1.order < item2.order
        }
        return params
    }
    
    func getCppPrivateParamsEnumCases() -> [String : AstNode] {
        var enumCases = [String : AstNode]()
        for (_, node) in self where node.isMainNode {
            if node.type == .enumerate {
                let isAnonymous = node.name.contains("AnonymousEnum")
                let prefix = "\(node.parentName!)" + (isAnonymous ? "" : "::\(node.name)")
                for item in node.children {
                    let key = "\(prefix)::\(item.name)"
                    enumCases[key] = node
                }
            } else if node.type == .class {
                for subNode in node.children where subNode.type == .enumerate {
                    let isAnonymous = subNode.name.contains("AnonymousEnum")
                    let prefix = "\(node.parentName!)::\(node.name)" + (isAnonymous ? "" : "::\(subNode.name)")
                    for item in subNode.children {
                        let key = "\(prefix)::\(item.name)"
                        enumCases[key] = subNode
                    }
                }
            }
        }
        return enumCases
    }
}

////////////////////////////////////////////////////

class APIsCPPParser {
    private let clangAstReader: LineReader!
    private let jsonFilePath: String!
    private let namespaceRootKey: String!
    // regex
    private let namespaceRegex = try! Regex(pattern: #">\s+line:\d+:\d+\s+(.+?)\n"#,
                                            groupNames: ["name"])
    private let enumRegex = try! Regex(pattern: #">\s+((col:\d+)|(line:\d+:\d+))\s+(.+?\s+)?(class\s+)?(.+?)\s*('.+?')?\n"#,
                                       groupNames: "source", "col", "line", "symbol", "classEnum", "name", "type")
    private let anonymousEnumRegex = try! Regex(pattern: #"((col:\d+)|(line:\d+:\d+))\n"#,
                                                groupNames: ["source", "col", "line"])
    private let propertyCaseRegex = try! Regex(pattern: #">\s+(col|line):.+?\s+(referenced\s+)?(.+?)\s+'(.+?)'(\s+(.+?))?\n"#,
                                               groupNames: ["location", "referenced", "name", "type", "propSymbol", "symbol"])
    private let anonymousCaseRegex = try! Regex(pattern: #">\s+.+?\s+(.+?)\s+'(.+?)'\n"#,
                                                groupNames: ["name", "type"])
    private let literalValueRegex = try! Regex(pattern: #">\s+'(.+?)'\s+(.+?)\n"#,
                                               groupNames: ["rawType", "value"])
    private let classRegex = try! Regex(pattern: #">.+?\s+(struct|class)\s(.+?)(\s+definition)?\n"#,
                                        groupNames: ["type", "name", "defined"])
    private let methodRegex = try! Regex(pattern: #">\s+((col:\d+)|(line:\d+:\d+))\s+(.+?)\s+'((.+?)\s*\(.+?)'(\s+(.+?))?\n"#,
                                         groupNames: ["source", "col", "line", "name",
                                                      "signature", "returnType", "methodSymbol", "symbol"])
    private let paramRegex = try! Regex(pattern: #">\s+((col:\d+)|(.+?\.h:\d+:\d+))\s+(.+?)\s+'(.+?)'(\s+.+?)?\n"#,
                                        groupNames: ["source", "col", "line", "name", "type", "symbol"])
    
    init(namespaceRootKey: String, dumpFilePath: String?=nil, jsonFilePath: String?=nil) {
        assert(namespaceRootKey.count > 0, "parser need keyWords count > 0")
        self.jsonFilePath = jsonFilePath
        self.namespaceRootKey = namespaceRootKey
        self.clangAstReader = self.jsonFilePath == nil ? LineReader(path: dumpFilePath!) : nil
    }
    
    func parse(accessPrivate: Bool=false, ignoreMethods: Bool=false) -> [String : AstNode] {
        if let jsonFilePath = self.jsonFilePath,
            let data = try? Data(contentsOf: URL(fileURLWithPath: jsonFilePath)),
            let nodes = try? JSONDecoder().decode([String: AstNode].self , from: data) {
            // set methods & properties index
            for (_, node) in nodes where node.type == .interface || node.type == .protocol {
                var methodIndex = 1
                var propertyIndex = 1
                for subNode in node.children {
                    if subNode.type == .method {
                        subNode.order = methodIndex
                        methodIndex += 1
                    } else if subNode.type == .property {
                        subNode.order = propertyIndex
                        propertyIndex += 1
                    }
                }
            }
            return nodes
        }
        
        var nodes = [String : AstNode]()
        if self.clangAstReader != nil {
            // for handle namespace
            var parsingNamespace = false
            var parsingContent = false
            var currNamespacePrefix: String! = nil
            var currNamespace: String! = nil
            // for detect class scope, like IAgoraService : (4, protected)
            var scopeIndents = [String : (Int, Bool)]()
            // for handle template class/struct
            var prepareHandleTemplateClassName = ""
            // for anonymous enum
            var anonymousPositions = [String : Int]()
            // save node to stack
            let stack = Stack<AstNode>()
            var currNode: AstNode? = nil
            for line in self.clangAstReader {
                autoreleasepool {
                    if line.contains("NamespaceDecl") {
                        let parsingRootNamespace = line.starts(with: "|-NamespaceDecl") || line.starts(with: "`-NamespaceDecl")
                        if parsingRootNamespace || parsingNamespace {
                            let match = namespaceRegex.findFirst(in: line)!
                            let name = match.group(named: "name")!
                            if parsingRootNamespace {
                                if name == self.namespaceRootKey {
                                    parsingNamespace = true
                                    currNamespacePrefix = self.namespaceRootKey
                                } else {
                                    parsingNamespace = false
                                    currNamespacePrefix = nil
                                }
                                parsingContent = false
                                currNamespace = nil
                            } else if parsingNamespace, var prefix = currNamespacePrefix {
                                if currNamespace != nil {
                                    if parsingContent {
                                        currNamespace = nil
                                        parsingContent = false
                                    } else {
                                        currNamespacePrefix = currNamespace
                                        prefix = currNamespace!
                                    }
                                }
                                currNamespace = "\(prefix)::\(name)"
                                let indent = getScopeIndent(with: line)
                                if scopeIndents[currNamespace] == nil {
                                    scopeIndents[currNamespace] = (indent, false)
                                }
                            }
                        }
                    } else if currNamespace != nil {
                        if line.contains("EnumDecl") {
                            if let node = currNode, stack.count > 0 {
                                if !node.isMainNode {
                                    currNode = APIsParser.getMainNode(stack: stack, nodes: &nodes)
                                }
                                if let top = currNode,
                                    top.type == .enumerate
                                    || (top.type == .class
                                        && top.parentNodeType! == .namespace && top.parentName! != currNamespace!) {
                                    APIsParser.startParseMainNode(stack: stack, currNode: &currNode, nodes: &nodes)
                                }
                            }
                            parsingContent = true
                            if nodes[currNamespace] == nil {
                                let namespaceNode = AstNode(type: .namespace, name: currNamespace)
                                nodes[currNamespace] = namespaceNode
                            }
                            
                            let indent = self.getScopeIndent(with: line)
                            var anonymousName = ""
                            var match = enumRegex.findFirst(in: line)
                            if match == nil && (stack.count == 0 || (stack.top()!.type == .class)),
                                    let namespaceIndent = scopeIndents[currNamespace]?.0 {
                                match = anonymousEnumRegex.findFirst(in: line)
                                if stack.count == 0 || (namespaceIndent + 1) == indent {
                                    let anonymousPos = anonymousPositions[currNamespace!] ?? 1
                                    anonymousName = "\(currNamespace!)_AnonymousEnum\(anonymousPos)"
                                    anonymousPositions[currNamespace!] = anonymousPos + 1
                                } else {
                                    let anonymousPos = anonymousPositions[stack.top()!.name] ?? 1
                                    anonymousName = "\(stack.top()!.name)_AnonymousEnum\(anonymousPos)"
                                    anonymousPositions[stack.top()!.name] = anonymousPos + 1
                                }
                            }
                            if let match = match {
                                var enumNode: AstNode! = nil
                                if anonymousName.count > 0 {
                                    enumNode = AstNode(type: .enumerate, name: anonymousName)
                                } else {
                                    let name = match.group(named: "name")!
                                    enumNode = AstNode(type: .enumerate, name: name)
                                    if let type = match.group(named: "type") {
                                        enumNode.declType = type.trimmingCharacters(in: ["'"])
                                    }
                                }
                                // detect scope
                                if let parentNode = stack.top(), parentNode.type == .class,
                                    let scopeInfo = scopeIndents[parentNode.name], indent > scopeInfo.0 {
                                    enumNode.parentName = parentNode.name
                                    enumNode.parentNodeType = parentNode.type
                                    parentNode.children.append(enumNode)
                                } else {
                                    enumNode.parentName = currNamespace
                                    enumNode.parentNodeType = .namespace
                                    APIsParser.startParseMainNode(stack: stack, currNode: &currNode, nodes: &nodes)
                                }
                                currNode = enumNode
                                stack.push(item: currNode!)
                            } else {
                                print("TODO: remove, enum \(line)")
                            }
                        } else if line.contains("ClassTemplateDecl ") {
                            let match = namespaceRegex.findFirst(in: line)!
                            let name = match.group(named: "name")!
                            prepareHandleTemplateClassName = "\(currNamespace!).\(name)"
                        } else if line.contains("CXXRecordDecl") {
                            if let node = currNode,
                                node.type != .class || (line.contains("definition") && node.parentNodeType! == .class) {
                                currNode = APIsParser.getMainNode(stack: stack, nodes: &nodes, findMainNodeType: .class)
                            }
                            parsingContent = true
                            if nodes[currNamespace] == nil {
                                let namespaceNode = AstNode(type: .namespace, name: currNamespace)
                                nodes[currNamespace] = namespaceNode
                            }
                            // ignore union
                            if let match = classRegex.findFirst(in: line) {
                                let type = match.group(named: "type")!
                                let name = match.group(named: "name")!
                                // ignore template class/struct like `AutoPtr`
                                if !(prepareHandleTemplateClassName.contains(name)
                                    && prepareHandleTemplateClassName.contains(currNamespace)) {
                                    if match.group(named: "defined") != nil {
                                        let classNode = AstNode(type: .class, name: name)
                                        classNode.declType = type
                                        // detect scope
                                        let indent = getScopeIndent(with: line)
                                        if let parentNode = stack.top(), parentNode.type == .class,
                                            let scopeInfo = scopeIndents[parentNode.name], indent > scopeInfo.0 {
                                            classNode.parentName = parentNode.name
                                            classNode.parentNodeType = parentNode.type
                                            parentNode.children.append(classNode)
                                        } else {
                                            APIsParser.startParseMainNode(stack: stack, currNode: &currNode, nodes: &nodes)
                                            classNode.parentName = currNamespace
                                            classNode.parentNodeType = .namespace
                                        }
                                        scopeIndents[name] = (indent, type == "struct")
                                        currNode = classNode
                                        stack.push(item: classNode)
                                    }
                                } else {
                                    prepareHandleTemplateClassName = ""
                                }
                            }
                        } else if line.contains("AccessSpecDecl") {
                            if let classNode = stack.top(), classNode.type == .class,
                                let scopeInfo = scopeIndents[classNode.name] {
                                let indent = getScopeIndent(with: line)
                                if indent - 1 == scopeInfo.0 {
                                    scopeIndents[classNode.name] = (scopeInfo.0, line.contains("public"))
                                }
                            }
                        } else if line.contains("EnumConstantDecl"), currNode != nil {
                            if !currNode!.isMainNode {
                                currNode = stack.peek(num: 1)
                            }
                            if let node = currNode, node.type == .enumerate {
                                var caseName: String! = nil
                                var enumName: String! = nil
                                // handle anonymous enum case by macro create like: DECLARE_PACKET_2 in cache_manager.h
                                if !(line.contains("> col:") || line.contains("> line:")) {
                                    let match = anonymousCaseRegex.findFirst(in: line)!
                                    caseName = match.group(named: "name")!
                                    enumName = match.group(named: "type")!
                                    if enumName.contains("anonymous") {
                                        enumName = node.name
                                    }
                                } else {
                                    let match = propertyCaseRegex.findFirst(in: line)!
                                    caseName = match.group(named: "name")!
                                    enumName = match.group(named: "type")!
                                    if enumName.contains("anonymous") {
                                        enumName = node.name
                                    }
                                }
                                if enumName.contains(node.name) {
                                    let caseNode = AstNode(type: .property, name: caseName)
                                    caseNode.parentName = node.name
                                    caseNode.parentNodeType = node.type
                                    // push to stack
                                    node.children.append(caseNode)
                                    currNode = caseNode
                                    stack.push(item: caseNode)
                                }
                            }
                        } else if (line.contains("CXXMethodDecl") || line.contains("CXXConstructorDecl"))
                                && currNode != nil {
                            if !currNode!.isMainNode {
                                currNode = APIsParser.getMainNode(stack: stack, nodes: &nodes, findMainNodeType: .class)
                            }
                            var canAccess = false
                            if let name = currNode?.name, let scopeInfo = scopeIndents[name],
                                (accessPrivate || (!accessPrivate && scopeInfo.1)) {
                                canAccess = true
                            }
                            canAccess = canAccess && !ignoreMethods
                            // virtual pure && static
                            if canAccess, var node = currNode, node.type == .class, let scopeInfo = scopeIndents[node.name],
                                (line.contains("virtual pure") || line.contains("static")) {
                                let indent = getScopeIndent(with: line)
                                if indent - 1 != scopeInfo.0 {
                                    currNode = APIsParser.getMainNode(stack: stack, nodes: &nodes, findMainNodeType: .class)
                                    node = currNode!
                                }
                                let match = methodRegex.findFirst(in: line)!
                                let name = match.group(named: "name")!
                                let signature = match.group(named: "signature")!
                                let returnType = match.group(named: "returnType")!
                                let symbol = match.group(named: "symbol") ?? ""
                                let methodNode = AstNode(type: .method, name: name)
                                methodNode.declType = returnType
                                methodNode.parentName = node.name
                                methodNode.parentNodeType = node.type
                                let valueNode = AstNodeValue()
                                valueNode.valueType = returnType
                                valueNode.isStaticValue = symbol == "static"
                                let signatureNode = AstNode(type: .method, name: signature)
                                valueNode.blockSignature = signatureNode
                                methodNode.value = valueNode
                                // push to stack
                                node.children.append(methodNode)
                                currNode = methodNode
                                stack.push(item: methodNode)
                            }
                        } else if line.contains("FunctionDecl") {
                            APIsParser.startParseMainNode(stack: stack, currNode: &currNode, nodes: &nodes)
                            if line.contains("Agora") {
                                parsingContent = true
                                if nodes[currNamespace] == nil {
                                    let namespaceNode = AstNode(type: .namespace, name: currNamespace)
                                    nodes[currNamespace] = namespaceNode
                                }
                                let match = methodRegex.findFirst(in: line)!
                                let name = match.group(named: "name")!
                                let signature = match.group(named: "signature")!
                                let returnType = match.group(named: "returnType")!
                                
                                let funcNode = AstNode(type: .function, name: name)
                                funcNode.declType = returnType
                                funcNode.parentName = currNamespace
                                funcNode.parentNodeType = .namespace
                                let valueNode = AstNodeValue()
                                valueNode.valueType = returnType
                                let signatureNode = AstNode(type: .method, name: signature)
                                valueNode.blockSignature = signatureNode
                                funcNode.value = valueNode
                                // push to stack
                                currNode = funcNode
                                stack.push(item: funcNode)
                            }
                        } else if line.contains("ParmVarDecl"), let node = currNode {
                            if node.type == .method || node.type == .function {
                                let match = paramRegex.findFirst(in: line)!
                                let name = match.group(named: "name")!
                                let type = match.group(named: "type")!
                                let symbol = match.group(named: "symbol") ?? ""
                                let paramNode = AstNode(type: .parameter, name: name)
                                paramNode.declType = type
                                paramNode.parentName = node.name
                                paramNode.parentNodeType = node.type
                                node.children.append(paramNode)
                                // need parse default value
                                if symbol.trimmingCharacters(in: .whitespacesAndNewlines) == "cinit" {
                                    currNode = paramNode
                                    stack.push(item: paramNode)
                                }
                            }
                        } else if line.contains("FieldDecl"), currNode != nil {
                            if !currNode!.isMainNode {
                                currNode = APIsParser.getMainNode(stack: stack, nodes: &nodes, findMainNodeType: .class)
                            }
                            var canAccess = false
                            if let name = currNode?.name, let scopeInfo = scopeIndents[name],
                                (accessPrivate || (!accessPrivate && scopeInfo.1)) {
                                canAccess = true
                            }
                            if canAccess, let node = currNode, node.type == .class {
                                let match = propertyCaseRegex.findFirst(in: line)!
                                let name = match.group(named: "name")!.replacingOccurrences(of: "invalid ", with: "")
                                let type = match.group(named: "type")!
                                let propNode = AstNode(type: .property, name: name)
                                propNode.declType = type
                                propNode.parentName = node.name
                                propNode.parentNodeType = node.type
                                node.children.append(propNode)
                            }
                        } else if let node = currNode, (node.type == .property || node.type == .parameter) {
                            var value: String? = nil
                            var rawType: String? = nil
                            if line.contains("ConstantExpr") {
                                let match = literalValueRegex.findFirst(in: line)!
                                rawType = match.group(named: "rawType")!
                                value = match.group(named: "value")!
                                if let split = value?.components(separatedBy: ":"), split.count == 2 {
                                    value = split[1].trimmingCharacters(in: .whitespaces)
                                }
                            }
                            if let value = value {
                                let valueNode = AstNodeValue()
                                valueNode.isValueNonnull = true
                                valueNode.valueType = rawType
                                valueNode.value = value
                                node.value = valueNode
                                node.declType = rawType
                                if node.type == .parameter {
                                    currNode = stack.peek(num: 1)
                                }
                            }
                        }
                    }
                }
            }
            APIsParser.handleLastNode(currNode, stack: stack, nodes: &nodes)
        }
        // handle enum's auto-increment case
        for (_, node) in nodes where node.type == .enumerate {
            var currValue = -1
            for item in node.children {
                if let value = item.value {
                    currValue = Int(value.value!)!
                } else {
                    currValue += 1
                    let valueNode = AstNodeValue()
                    valueNode.isValueNonnull = true
                    valueNode.valueType = node.declType
                    valueNode.value = "\(currValue)"
                    item.value = valueNode
                }
            }
        }
        return nodes
    }
    
    private func getScopeIndent(with line: String) -> Int {
        var indent = 0
        // detect "|   |" == "| ` |" == 3
        var blackSpace = 0
        for char in line {
            if char == "|" || char == "`" {
                if blackSpace >= 3 {
                    let missingIndent = Int(Float(blackSpace) / 2.0)
                    indent += 1 + missingIndent
                } else {
                    indent += 1
                }
                blackSpace = 0
            } else if char == " " {
                blackSpace += 1
            } else if char == "-" {
                break
            }
        }
        return indent
    }
    
    func parsePrivateParams(headers: [(String, String)]) -> [String : AstNode] {
        var exprRegex: Regex! = nil
        let exprTypeRegex = try! Regex(pattern: #"<(.+?)>((\s+)|(':'))"#, groupNames: ["type"])
        let exprValueEnumRegex = try! Regex(pattern: #"(^|\)|\s+|\||,|\<)([A-Za-z][\w:]+)($|,|\s+|\)|\|)"#,
                                            groupNames: ["prefix", "name", "suffix"])
        let commentRegex = try! Regex(pattern: #"\s*(//\s*.+?)\n"#, groupNames: ["comment"])
        let classRegex = try! Regex(pattern: #"class\s+(.+?)((\s+:\s+)|(\s+\{)?\n)"#, groupNames: ["name"])
        let formatScopeEndRegex = try! Regex(pattern: #"(^\s+,\s*[a-z])|(((\d|\})\),?\s*)|(\",\s*\{)\n)"#, groupNames: [])
        
        let nodes = self.parse(accessPrivate: true, ignoreMethods: true)
        let allEnumCases = nodes.getCppPrivateParamsEnumCases()
        var index = 1
        for (header, keyword) in headers {
            let paramExprKeyword1 = "(\(keyword), "
            let paramExprKeyword2 = "(\(keyword),"
            var rootClassScopeKeyword: String! = nil
            if keyword == "mp" {
                exprRegex = try! Regex(pattern: #"(\s+:\s*|,\s*)(.+?)\(mp,\s*\"(.+?)\"(,\s*(.+?))?\)\s*\n"#,
                                       groupNames: ["start", "propName", "name", "valuesPart", "values", "comment"])
                rootClassScopeKeyword = "(agora::base::ConfigEngine& mp)"
            } else {
                exprRegex = try! Regex(pattern: #"(\s+:\s*|,\s*)(.+?)\(configEngine,\s*\"(.+?)\"(,\s*(.+?))?\)\s*\n"#,
                                       groupNames: ["start", "propName", "name", "valuesPart", "values", "comment"])
                rootClassScopeKeyword = "(config::IConfigEngine& configEngine)"
            }

            autoreleasepool {
                var lines = [String]()
                // format header file lines
                if let reader = LineReader(path: header) {
                    var lastComma = false
                    var lastBrace = false
                    var prepareHandleLine: String! = nil
                    var waitLines = [String]()
                    for line in reader {
                        let commentMatch = commentRegex.findFirst(in: line)?.group(named: "comment") ?? ""
                        var currLine = commentMatch == "" ? line : line.replacingOccurrences(of: commentMatch, with: "")
                        if lastComma || lastBrace {
                            if formatScopeEndRegex ~= line {
                                if lastComma && waitLines.count == 0 {
                                    let handledLine = prepareHandleLine.replacingOccurrences(of: ",\n", with: "\n")
                                    lines.append(handledLine)
                                    currLine = line.trimmingCharacters(in: .whitespaces)
                                    currLine = "    , \(currLine)"
                                } else if lastBrace || waitLines.count > 0 {
                                    currLine = prepareHandleLine.trimmingCharacters(in: .newlines)
                                        + waitLines.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.joined()
                                    if lastBrace {
                                        currLine += line.trimmingCharacters(in: .whitespaces)
                                    } else {
                                        lines.append(currLine + "\n")
                                        currLine = line
                                    }
                                    waitLines.removeAll()
                                }
                                lastComma = false
                                lastBrace = false
                                prepareHandleLine = nil
                            } else {
                                waitLines.append(currLine)
                                continue
                            }
                        }
                        if currLine.contains(paramExprKeyword1) || currLine.contains(paramExprKeyword2) {
                            if currLine.hasSuffix(",\n") {
                                prepareHandleLine = currLine
                                lastComma = true
                            } else if currLine.hasSuffix("{\n") {
                                prepareHandleLine = currLine
                                lastBrace = true
                            }
                        }
                        if !(lastComma || lastBrace) {
                            lines.append(currLine)
                        }
                    }
                }
                // parse params
                var rootClassNode: AstNode! = nil
                var rootClassScopeEnd: String! = nil
                var currClassNode: AstNode! = nil
                var currClassScopeBegin: String! = nil
                var startParse = false
                for line in lines {
                    if line.starts(with: "class") {
                        let match = classRegex.findFirst(in: line)!
                        let rootClassName = match.group(named: "name")!

                        rootClassScopeEnd = "\(rootClassName)\(rootClassScopeKeyword!)"
                        for (key, node) in nodes {
                            if key.hasSuffix("::\(rootClassName)") {
                                if self.findParamsSubClassNode(node, &currClassNode, &currClassScopeBegin,
                                                               rootClassScopeKeyword: rootClassScopeKeyword) {
                                    rootClassNode = node
                                    let valueNode = AstNodeValue()
                                    valueNode.valueType = "RootParamsClass"
                                    rootClassNode.value = valueNode
                                    break
                                }
                            }
                        }
                    } else if rootClassNode != nil {
                        if line.contains(currClassScopeBegin) {
                            startParse = true
                        }
                        if line.contains("{}\n") {
                            startParse = false
                            let findCurrNode = self.findParamsSubClassNode(rootClassNode, &currClassNode, &currClassScopeBegin,
                                                                           rootClassScopeKeyword: rootClassScopeKeyword)
                            if !findCurrNode {
                                currClassNode = nil
                                currClassScopeBegin = nil
                            }
                        }
                        if startParse && (line.contains(paramExprKeyword1) || line.contains(paramExprKeyword2)) {
                            let exprMatch = exprRegex.findFirst(in: line)!
                            let propName = exprMatch.group(named: "propName")!
                            let name = exprMatch.group(named: "name")!
                            let values = exprMatch.group(named: "values") ?? ""
                            // parse expr type
                            var type: String! = nil
                            for fieldNode in currClassNode.children {
                                if fieldNode.name == propName {
                                    // parse type
                                    let declType = fieldNode.declType!
                                    if let typeMatch = exprTypeRegex.findFirst(in: declType) {
                                        type = typeMatch.group(named: "type")!
                                    } else // handle invalid field type
                                        if let fieldLine = lines.first(where: { $0.hasSuffix(" \(propName);\n") }),
                                            let rawType = exprTypeRegex.findFirst(in: fieldLine)?.group(named: "type") {
                                        let split = rawType.components(separatedBy: ",")
                                        if split.count == 2 && split[0].contains("std::list<") {
                                            type = "std::list<\(split[1].trimmingCharacters(in: .whitespaces))>"
                                        }
                                    }
                                    // save to valueNode
                                    let valueNode = AstNodeValue()
                                    valueNode.valueType = "ParamField"
                                    valueNode.rawType = type
                                    valueNode.value = values
                                    valueNode.typeEncoding = name
                                    valueNode.valueValidate = "\(rootClassNode.parentName!)::\(rootClassNode.name)::\(currClassNode.name)"
                                    fieldNode.value = valueNode
                                    fieldNode.order = index
                                    index += 1
                                    // find enum value type
                                    let jsonType = fieldNode.cppPrivateParamJsonTypeTips
                                    if jsonType.contains("Number") {
                                        var findEnumCaseName: String! = nil
                                        if let match = exprValueEnumRegex.findFirst(in: values),
                                            let enumCaseName = match.group(named: "name"),
                                            !(enumCaseName == "true" || enumCaseName == "false") {
                                            findEnumCaseName = enumCaseName
                                            for (key, node) in allEnumCases {
                                                if key.contains(enumCaseName) {
                                                    fieldNode.value?.blockSignature = node
                                                    break
                                                }
                                            }
                                            if fieldNode.value?.blockSignature == nil, let enumCaseName = findEnumCaseName {
                                                // handle like `rtc::AREA_CODE_GLOB`
                                                if enumCaseName.contains("::") {
                                                    let split = Array(enumCaseName.components(separatedBy: "::").reversed())
                                                    if split.count > 1 {
                                                        findEnumCaseName = split[0]
                                                        for (key, node) in allEnumCases {
                                                            if key.contains(findEnumCaseName) {
                                                                fieldNode.value?.blockSignature = node
                                                                break
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    break
                                }
                            }
                            if type == nil {
                                print(">>>>>> not found \(name) params")
                            }
                        }
                        
                        if line.contains(rootClassScopeEnd) {
                            rootClassNode = nil
                            rootClassScopeEnd = nil
                            currClassNode = nil
                            currClassScopeBegin = nil
                        }
                    }
                }
            }
        }
        return nodes
    }
    
    @discardableResult
    private func findParamsSubClassNode(_ rootNode: AstNode,
                                        _ currClassNode: inout AstNode?,
                                        _ currClassScopeBegin: inout String?,
                                        rootClassScopeKeyword: String) -> Bool {
        var findCurrNode = false
        for node in rootNode.children where node.type == .class {
            if currClassNode == nil {
                findCurrNode = true
            } else if let currClassNode = currClassNode, node.name == currClassNode.name {
                findCurrNode = true
                continue
            }
            if findCurrNode {
                let valueNode = AstNodeValue()
                valueNode.valueType = "SubParamsClass"
                node.value = valueNode
                currClassNode = node
                currClassScopeBegin = "\(currClassNode!.name)\(rootClassScopeKeyword)"
                break
            }
        }
        return findCurrNode
    }
}
